1
|
|
|
/******************************************* |
2
|
|
|
* * |
3
|
|
|
* This util was created by Marius Olbertz * |
4
|
|
|
* Please thank Marius on GitHub /owlbertz * |
5
|
|
|
* or the web http://www.mariusolbertz.de/ * |
6
|
|
|
* * |
7
|
|
|
******************************************/ |
8
|
|
|
|
9
|
|
|
'use strict'; |
10
|
|
|
|
11
|
|
|
import { rtl as Rtl } from './siteapp.util.core'; |
12
|
|
|
|
13
|
|
|
const keyCodes = { |
14
|
|
|
9 : 'TAB', |
15
|
|
|
13: 'ENTER', |
16
|
|
|
27: 'ESCAPE', |
17
|
|
|
32: 'SPACE', |
18
|
|
|
35: 'END', |
19
|
|
|
36: 'HOME', |
20
|
|
|
37: 'ARROW_LEFT', |
21
|
|
|
38: 'ARROW_UP', |
22
|
|
|
39: 'ARROW_RIGHT', |
23
|
|
|
40: 'ARROW_DOWN' |
24
|
|
|
} |
25
|
|
|
|
26
|
|
|
let commands = {} |
27
|
|
|
|
28
|
|
|
// Functions pulled out to be referenceable from internals |
29
|
|
|
function findFocusable($element) { |
30
|
|
|
if(!$element) {return false; } |
31
|
|
|
return $element.find('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]').filter(function() { |
32
|
|
|
if (!$(this).is(':visible') || $(this).attr('tabindex') < 0) { return false; } //only have visible elements and those that have a tabindex greater or equal 0 |
33
|
|
|
return true; |
34
|
|
|
}); |
35
|
|
|
} |
36
|
|
|
|
37
|
|
|
function parseKey(event) { |
38
|
|
|
var key = keyCodes[event.which || event.keyCode] || String.fromCharCode(event.which).toUpperCase(); |
39
|
|
|
|
40
|
|
|
// Remove un-printable characters, e.g. for `fromCharCode` calls for CTRL only events |
41
|
|
|
key = key.replace(/\W+/, ''); |
42
|
|
|
|
43
|
|
|
if (event.shiftKey) key = `SHIFT_${key}`; |
|
|
|
|
44
|
|
|
if (event.ctrlKey) key = `CTRL_${key}`; |
|
|
|
|
45
|
|
|
if (event.altKey) key = `ALT_${key}`; |
|
|
|
|
46
|
|
|
if (event.metaKey) key = `META_${key}`; |
|
|
|
|
47
|
|
|
|
48
|
|
|
// Remove trailing underscore, in case only modifiers were used (e.g. only `CTRL_ALT`) |
49
|
|
|
key = key.replace(/_$/, ''); |
50
|
|
|
|
51
|
|
|
return key; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
const Keyboard = { |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var {object} keys - keycodes map |
58
|
|
|
*/ |
59
|
|
|
keys: getKeyCodes(keyCodes), |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Parses the (keyboard) event and returns a String that represents its key |
63
|
|
|
* Can be used like Foundation.parseKey(event) === Foundation.keys.SPACE |
64
|
|
|
* @param {Event} event - the event generated by the event handler |
65
|
|
|
* @return String key - String that represents the key pressed |
66
|
|
|
*/ |
67
|
|
|
parseKey: parseKey, |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Handles the given (keyboard) event |
71
|
|
|
* @param {Event} event - the event generated by the event handler |
72
|
|
|
* @param {String} component - Foundation component's name, e.g. Slider or Reveal |
73
|
|
|
* @param {Objects} functions - collection of functions that are to be executed |
74
|
|
|
*/ |
75
|
|
|
handleKey(event, component, functions) { |
76
|
|
|
var commandList = commands[component], |
77
|
|
|
keyCode = this.parseKey(event), |
78
|
|
|
cmds, |
79
|
|
|
command, |
80
|
|
|
fn; |
81
|
|
|
|
82
|
|
|
if (!commandList) return console.warn('Component not defined!'); |
|
|
|
|
83
|
|
|
|
84
|
|
|
if (typeof commandList.ltr === 'undefined') { // this component does not differentiate between ltr and rtl |
85
|
|
|
cmds = commandList; // use plain list |
86
|
|
|
} else { // merge ltr and rtl: if document is rtl, rtl overwrites ltr and vice versa |
87
|
|
|
if (Rtl()) cmds = $.extend({}, commandList.ltr, commandList.rtl); |
|
|
|
|
88
|
|
|
|
89
|
|
|
else cmds = $.extend({}, commandList.rtl, commandList.ltr); |
90
|
|
|
} |
91
|
|
|
command = cmds[keyCode]; |
92
|
|
|
|
93
|
|
|
fn = functions[command]; |
94
|
|
|
if (fn && typeof fn === 'function') { // execute function if exists |
95
|
|
|
var returnValue = fn.apply(null, [event]); |
96
|
|
|
if (functions.handled || typeof functions.handled === 'function') { // execute function when event was handled |
|
|
|
|
97
|
|
|
functions.handled(returnValue); |
|
|
|
|
98
|
|
|
} |
99
|
|
|
} else { |
100
|
|
|
if (functions.unhandled || typeof functions.unhandled === 'function') { // execute function when event was not handled |
|
|
|
|
101
|
|
|
functions.unhandled(); |
|
|
|
|
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
}, |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Finds all focusable elements within the given `$element` |
108
|
|
|
* @param {jQuery} $element - jQuery object to search within |
109
|
|
|
* @return {jQuery} $focusable - all focusable elements within `$element` |
110
|
|
|
*/ |
111
|
|
|
|
112
|
|
|
findFocusable: findFocusable, |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Stores set of commands mapped to handler names in a given namespace |
116
|
|
|
* |
117
|
|
|
* @param {string} namespace - keyboard event/command namespace |
118
|
|
|
* @param {object} cmds - handler name to command map |
119
|
|
|
*/ |
120
|
|
|
|
121
|
|
|
register (namespace, cmds) { |
122
|
|
|
commands[namespace] = cmds; |
123
|
|
|
//console.//log('registered keyboard component:', namespace, cmds, commands); |
124
|
|
|
}, |
125
|
|
|
|
126
|
|
|
|
127
|
|
|
// TODO9438: These references to Keyboard need to not require global. Will 'this' work in this context? |
128
|
|
|
// |
129
|
|
|
/** |
130
|
|
|
* Traps the focus in the given element. |
131
|
|
|
* @param {jQuery} $element jQuery object to trap the foucs into. |
132
|
|
|
*/ |
133
|
|
|
trapFocus($element) { |
134
|
|
|
var $focusable = findFocusable($element), |
135
|
|
|
$firstFocusable = $focusable.eq(0), |
136
|
|
|
$lastFocusable = $focusable.eq(-1); |
137
|
|
|
|
138
|
|
|
$element.on('keydown.'+Keyboard._ns+'.trapfocus', function(event) { |
139
|
|
|
if (event.target === $lastFocusable[0] && parseKey(event) === 'TAB') { |
140
|
|
|
event.preventDefault(); |
141
|
|
|
$firstFocusable.focus(); |
142
|
|
|
} |
143
|
|
|
else if (event.target === $firstFocusable[0] && parseKey(event) === 'SHIFT_TAB') { |
144
|
|
|
event.preventDefault(); |
145
|
|
|
$lastFocusable.focus(); |
146
|
|
|
} |
147
|
|
|
}); |
148
|
|
|
}, |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Releases the trapped focus from the given element. |
152
|
|
|
* @param {jQuery} $element jQuery object to release the focus for. |
153
|
|
|
*/ |
154
|
|
|
releaseFocus($element) { |
155
|
|
|
$element.off('keydown.'+Keyboard._ns+'.trapfocus'); |
156
|
|
|
}, |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Removes keyboard event handlers. If no namespace is given, all |
160
|
|
|
* keyboard handlers will be removed. |
161
|
|
|
* |
162
|
|
|
* @param {string} namespace - keyboard event namespace |
163
|
|
|
*/ |
164
|
|
|
unregister( namespace ) { |
165
|
|
|
if (namespace != '') { |
166
|
|
|
$(window).off('keydown.'+this.application.appName+'.'+namespace); |
167
|
|
|
if (typeof commands[namespace] != 'undefined') { |
168
|
|
|
delete commands[namespace]; |
169
|
|
|
} |
170
|
|
|
} else { |
171
|
|
|
$(window).off('keydown.'+this.application.appName); |
172
|
|
|
commands = {}; |
173
|
|
|
} |
174
|
|
|
}, |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Adds keyboard event handlers. |
178
|
|
|
* |
179
|
|
|
* handlers := { |
180
|
|
|
* '[KEY(S)]' : function myAction (event e) {...}, |
181
|
|
|
* ... |
182
|
|
|
* } |
183
|
|
|
* |
184
|
|
|
* @param {object} handlers - key(s) to action map |
185
|
|
|
* @param {string} namespace - keyboard event namespace |
186
|
|
|
*/ |
187
|
|
|
addKeyHandlers( handlers, namespace ) { |
188
|
|
|
|
189
|
|
|
var $this = this, |
190
|
|
|
$app = this._app, |
|
|
|
|
191
|
|
|
eventTrigger = 'keydown.'+this._ns, |
192
|
|
|
registeredKeys = {}, |
193
|
|
|
registeredHandlers = {} |
194
|
|
|
; |
195
|
|
|
|
196
|
|
|
if ('' == namespace) { |
197
|
|
|
let namespace = '_'+Siteapp.sys.genUUID(6); |
|
|
|
|
198
|
|
|
} |
199
|
|
|
eventTrigger = eventTrigger+'.'+namespace; |
200
|
|
|
|
201
|
|
|
// build 'reg keys' and 'hdl keys' maps |
202
|
|
|
for ( var key in handlers ) { |
203
|
|
|
if (handlers.hasOwnProperty(key)) { |
204
|
|
|
var handleName = namespace+''+key; |
205
|
|
|
registeredKeys[key] = handleName; |
206
|
|
|
registeredHandlers[handleName] = handlers[key]; |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
$(window).on(eventTrigger, function(e){ |
211
|
|
|
|
212
|
|
|
this.id = namespace+''+Siteapp.sys.genUUID(6); |
|
|
|
|
213
|
|
|
|
214
|
|
|
// ignore [TAB] key |
215
|
|
|
// if (e.which === 9) return; |
216
|
|
|
|
217
|
|
|
// handle keyboard event with keyboard util |
218
|
|
|
$this.handleKey(e, namespace, registeredHandlers); |
219
|
|
|
|
220
|
|
|
|
221
|
|
|
}); |
222
|
|
|
|
223
|
|
|
// register keyboard keys mapping |
224
|
|
|
this.register(namespace, registeredKeys); |
225
|
|
|
|
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
}; |
229
|
|
|
|
230
|
|
|
/* |
231
|
|
|
* Constants for easier comparing. |
232
|
|
|
* Can be used like Foundation.parseKey(event) === Foundation.keys.SPACE |
233
|
|
|
*/ |
234
|
|
|
function getKeyCodes(kcs) { |
235
|
|
|
var k = {}; |
236
|
|
|
for (var kc in kcs) k[kcs[kc]] = kcs[kc]; |
|
|
|
|
237
|
|
|
return k; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
export {Keyboard}; |
241
|
|
|
|
Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.
Consider:
If you or someone else later decides to put another statement in, only the first statement will be executed.
In this case the statement
b = 42
will always be executed, while the logging statement will be executed conditionally.ensures that the proper code will be executed conditionally no matter how many statements are added or removed.